/*  Copyright © 2009 Blake Hegerle -*- C++ -*- */

/*  This file is part of metal.
 
    Metal is free software: you can redistribute it and/or modify it
    under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    General Public License for more details.
 
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see
    <http://www.gnu.org/licenses/>.

*/

#include <cassert>
#include <iostream>
#include <type_traits>

#include "expr_impl.h"
#include "meta/expr.h"
#include "meta/type_check.h"
#include "type_code.h"
#include "imbue_type.h"
#include "identifier.h"
#include "expression.h"
#include "io.h"
#include "assignment.h"
#include "meta/type.h"
#include "struct.h"

using namespace metal;
using namespace metal::meta;
using namespace std;

namespace {
  ident(b);
  ident(x);
  ident(y);
  ident(f);
  ident(g);
}

namespace {
  typedef bindings<
    binding<meta_x, double>, 
    binding<meta_y, int>, 
    binding<meta_b, bool>,
    binding<meta_f, list<double> >,
    binding<meta_g, list<bool, int, double> >
  > scope;

  template<typename T>
  void check(expr_ptr expected, expr_ptr actual) {
    if(!expected->type())
      assert(0 && "no type set on expected expr");

    auto a = *imbue_type<T, scope>(actual);
    auto e = *expected;

    cout << "expected " << e << endl;
    cout << "actual   " << a << endl;

    assert(e == a);
  }
}

// double x
// int y
// double f()
// bool g(int, double, bool)

void type_imbued_expr_tests() {
  vector<expr_ptr> no_children;
  vector<assign_ptr> no_bindings;
  vector<param_impl> no_params;

  // make a constant expression, which is already imbued with a type -
  // so we're really making sure we don't blow that type away
  cout << "constant" << endl;
  check<constant_expr<double> >
    (new_ptr<constant_expr_impl>(3.0),
     new_ptr<constant_expr_impl>(3.0));

  // a simple reference to a bound identifier
  cout << "ident" << endl;
  auto expected_x=expr_ptr(new expr_impl(name_of<meta_x>(), no_params, no_children, no_bindings, lower<double>()));
  
  check<ident_expr<meta_x> >
    (expected_x,
     x.impl());

  cout << unary_expr_impl('+', x.impl()) << endl;
     
  // a simple (in this case, pointless) unary expression
  cout << "unary" << endl;
  auto expected_unary = expr_ptr(new expr_impl("+", no_params, { expected_x }, no_bindings, lower<double>()));

  check<unary_expr<arithmatic_tag, ident_expr<meta_x> > >
    (expected_unary,
     new_ptr<unary_expr_impl>('+', x.impl()));

  // a binary expression - the right operand is coerced
  cout << "binary" << endl;
  auto expected_y = expr_ptr(new expr_impl(name_of<meta_y>(), no_params, no_children, no_bindings, lower<int>()));
  auto expected_binary = expr_ptr(new expr_impl("-", no_params, { expected_x, expected_y }, 
						no_bindings, 
						lower<double>()));
   
  check<binary_expr<arithmatic_tag, ident_expr<meta_x>, ident_expr<meta_y> > >
    (expected_binary,
     new_ptr<binary_expr_impl>("-", x.impl(), y.impl()));

  // a nullary call
  cout << "nullary call" << endl;
  auto expected_f = expr_ptr(new expr_impl(name_of<meta_f>(),
					   no_params,
					   no_children,
					   no_bindings,
					   lower<metal::list<double> >()));

  auto exepcted_nullary_call = expr_ptr(new expr_impl("", 
						      no_params, 
						      { expected_f }, 
						      no_bindings, 
						      lower<double>()));

  initializer_list<expr_ptr> f_singleton({ f.impl() });
  check<call_expr<ident_expr<meta_f> > >
    (exepcted_nullary_call,
     new_ptr<call_expr_impl>(f_singleton.begin(), f_singleton.end()));

  // a binary call 
  cout << "binary call" << endl;

  auto expected_g = expr_ptr(new expr_impl(name_of<meta_g>(),
					   no_params,
					   no_children,
					   no_bindings,
					   lower<metal::list<bool, int, double> >()));

  auto expected_binary_call = expr_ptr(new expr_impl("",
						     no_params, 
						     { expected_g, expected_y, expected_x }, 
						     no_bindings, 
						     lower<bool>()));

  vector<expr_ptr> args({ g.impl(), expected_y, expected_x });
  check<call_expr<ident_expr<meta_g>, ident_expr<meta_y>, ident_expr<meta_x> > >
    (expected_binary_call,
     new_ptr<call_expr_impl>(args.begin(), args.end()));

  // eval - note that we shadow x so that when we do the addition, the
  // type of x gets promoted and not the type of y
  cout << "eval" << endl;

  auto short_x = expr_ptr(new expr_impl(name_of<meta_x>(),no_params, 
					no_children, no_bindings,
					lower<short>()));
  auto expected_scalar_binding 
    = assign_ptr(new assignment_impl(true, short_x, cnst_impl((short)1)));				     

  auto short_x_times_y = expr_ptr(new expr_impl(std::string("*"), no_params, 
						{ short_x, expected_y },
						no_bindings,
						lower<int>())) ;
  
  auto expected_eval
    = expr_ptr(new expr_impl("",no_params, 
			     { short_x_times_y },
			     vector<assign_ptr>({ expected_scalar_binding }),
			     lower<int>()));
  
  check<eval_expr<binary_expr<arithmatic_tag, ident_expr<meta_x>, ident_expr<meta_y> >, 
    binding<meta_x, constant_expr<short> > > >
    (expected_eval,
     eval(x * y, x = (short)1).impl());

  cout << "map" << endl;

  auto expected_array 
    = expr_ptr(new expr_impl("",no_params, 
			     { cnst_impl((short)1) },
			     no_bindings,
			     lower<short *>()));

  auto expected_array_assign =
    assign_ptr(new assignment_impl(false, x.impl(), expected_array));
  
  auto expected_map = 
    expr_ptr(new expr_impl("",no_params, 
			   { short_x },
			   vector<assign_ptr>({ expected_array_assign }),
			   lower<short *>()));
  
  check<map_expr<ident_expr<meta_x>, 
    binding<meta_x, array_expr<constant_expr<short> > > > >
    (expected_map,
     map(x, x *= metal::array((short)1)).impl());
  
#warning array type tree

  auto expected_b = expr_ptr(new expr_impl(name_of<meta_b>(),no_params, 
					   no_children, no_bindings,
					   lower<bool>()));
  auto expected_cond
    = expr_ptr(new expr_impl("",no_params, 
			     { expected_b, cnst_impl(1), cnst_impl(2) },
			     no_bindings,
			     lower<int>()));

  check<cond_expr<ident_expr<meta_b>, 
    constant_expr<int>, 
    constant_expr<int> > >
    (expected_cond, cond(b, 1, 2).impl());

  auto fltr = filter(b, b *= array(1), b < 2);

  cout << "e = " << fltr.impl()->bindings().size() << endl;
  cout << "s = " << fltr.impl()->children().size() << endl;
  cout << "zzz" << *fltr.impl() << endl;

  auto int_b = expr_ptr(new expr_impl(name_of<meta_b>(),no_params, 
				      no_children, no_bindings,
				      lower<int>()));

  auto singleton_int_one = expr_ptr(new expr_impl("",no_params, 
						  { cnst_impl(1) },
						  no_bindings,
						  lower<int *>()));
  
  auto range_var_b
    = assign_ptr(new assignment_impl(false, b.impl(), singleton_int_one));

  auto b_lt_two = expr_ptr(new expr_impl("<",no_params, 
					 { int_b, cnst_impl(2) },
					 no_bindings,
					 lower<bool>()));

  auto filter_b_in_singleton_int_one_lt_two
    = expr_ptr(new expr_impl("",no_params, 
			     { int_b, b_lt_two },
			     vector<assign_ptr>({ range_var_b }),
			     lower<int *>()));

  check<filter_expr<
    ident_expr<meta_b>,
    meta_b,
    array_expr<constant_expr<int> >,
    binary_expr<comparison_tag, ident_expr<meta_b>, constant_expr<int> > > >
  (filter_b_in_singleton_int_one_lt_two, fltr.impl());

#warning subscript type tree
#warning dot type tree

  deftype(s_type,
	  mem<int>(x),
	  mem<float>(y));

  auto s_x_assign =
    assign_ptr(new assignment_impl(true, x.impl(), cnst_impl(1)));

  auto s_y_assign =
    assign_ptr(new assignment_impl(true, y.impl(), cnst_impl(0.0f)));

  auto cons_s_type_y_x
    = expr_ptr(new expr_impl("",no_params, 
			     no_children,
			     { s_y_assign, s_x_assign },
			     lower<s_type>()));

  check<cons_expr<
    s_type,
    binding<meta_y, constant_expr<float> >,
    binding<meta_x, constant_expr<int> >
    >
  >
  (cons_s_type_y_x, cons<s_type>(y = 2, x = 1).impl());
      

#warning cast type tree

  cout << "type imbueing ok" << endl;
}
